﻿/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using Nvigorate.Common.Reflection;
using Nvigorate.Data;
using Nvigorate.Data.Query;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Navigation;
using Nvigorate.Relational.Provider;
using Nvigorate.Relational.Record;
using Nvigorate.Relational.UnitOfWork;
using Nvigorate.Relational.Visitors;
using SQL = Nvigorate.Data.Query;

namespace Nvigorate.Relational.Repository
{
    public class DatabaseRepository : BaseRepository, ITransactionManager
    {
        #region Fields

        protected DataInterface _database;

        #endregion

        #region Public Properties

        public IUnitOfWork UnitOfWork { get; set; }

        #endregion

        #region Private Properties

        private bool InTransaction { get; set; }

        #endregion

        #region IDatabaseRepository

        /// <summary>
        /// Deletes a single record from the source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="instance">The instance which represents the record to delete</param>
        public override int Delete<T>(T instance)
        {
            return Delete(instance, false);
        }

        /// <summary>
        /// Deletes a single record from the source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="instance">The instance which represents the record to delete</param>
        /// <param name="recursive">Deletes all instances in the hierarchy recursively</param>
        public override int Delete<T>(T instance, bool recursive)
        {
            UnitOfWork.Delete(instance, recursive, true);
            return 0;
        }

        /// <summary>
        /// Deletes a collection of records from the source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="collection">The collection which represents the records to delete</param>
        public override int Delete<T>(IEnumerable<T> collection)
        {
            return Delete(collection, false);
        }

        /// <summary>
        /// Deletes a collection of records from the source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="collection">The collection which represents the records to delete</param>
        /// <param name="recursive">Deletes all records in the hierarchy recursively</param>
        public override int Delete<T>(IEnumerable<T> collection, bool recursive)
        {
            int affected = 0;
            collection.ForEach(i => affected += Delete(i, recursive));
            return affected;
        }

        /// <summary>
        /// Retrieves the child instance for the subject object
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <typeparam name="S">The subject business object type</typeparam>
        /// <param name="subject">The parent instance</param>
        /// <returns>The child instance for the subject</returns>
        public override T GetRelative<T, S>(S subject)
        {
            return GetRelative(subject, new LoadInstruction<T>());
        }

        /// <summary>
        /// Retrieves the child instance for the subject object
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <typeparam name="S">The subject business object type</typeparam>
        /// <param name="subject">The parent instance</param>
        /// <param name="instructions">Specifies how records should be loaded and populated</param>
        /// <returns>The child instance for the subject</returns>
        public override T GetRelative<T, S>(S subject, LoadInstruction<T> instructions)
        {
            BuildParentClause(subject, instructions);
            return GetInstance(instructions);
        }

        /// <summary>
        /// Gets child collection for the subject object
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <typeparam name="S">The subject business object type</typeparam>
        /// <param name="subject">The parent instance</param>
        /// <returns>The child instance for the subject</returns>
        public override IList<T> GetRelatives<T, S>(S subject)
        {
            return GetRelatives(subject, new LoadInstruction<T>());
        }

        /// <summary>
        /// Gets child collection for the subject object
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <typeparam name="S">The subject business object type</typeparam>
        /// <param name="subject">The parent instance</param>
        /// <param name="instructions">Specifies how records should be loaded and populated</param>
        /// <returns>The child instance for the subject</returns>
        public override IList<T> GetRelatives<T, S>(S subject, LoadInstruction<T> instructions)
        {
            var manyToMany = BuildParentClause(subject, instructions);
            var result = Fetch<T>(instructions);
            var relatives = manyToMany ? (IList<T>) new StatefulList<T>(result) : result.ToList();
            return relatives;
        }

        /// <summary>
        /// Retrieve the first instance based on the search criteria provided
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="instructions">Specifies how records should be loaded and populated</param>
        /// <returns>The first instance matching the criteria provided</returns>
        public override T GetInstance<T>(LoadInstruction<T> instructions)
        {
            // limit the result to 1 record (at the top level)
            instructions.Page(1).PageBy(1);
            return Fetch<T>(instructions).FirstOrDefault() ?? new T();
        }

        /// <summary>
        /// Retrieve an instance based on the search criteria provided
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="id">The primary key value of the desired instance</param>
        /// <returns>The first instance matching the criteria provided</returns>
        public override T GetInstance<T>(object id)
        {
            return GetInstance(id, new LoadInstruction<T>());
        }

        /// <summary>
        /// Retrieve an instance based on the search criteria provided
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="id">The primary key value of the desired instance</param>
        /// <param name="instructions">Specifies how records should be loaded and populated</param>
        /// <returns>The first instance matching the criteria provided</returns>
        public override T GetInstance<T>(object id, LoadInstruction<T> instructions)
        {
            var type = typeof (T);
            var bindingMap = _mapIndex[_repositoryName, type];
            var keyAssignment = bindingMap.GetAssignmentsByPredicate(a => a.PrimaryKey).FirstOrDefault();

            if (keyAssignment == null)
                throw new RepositoryException(
                    "This form of GetInstance is invalid for type {0} because there is no primary key defined.",
                    type.FullName);

            var criteria = Criterion.Column(keyAssignment.Source.Field, keyAssignment.Source.Entity)
                .EqualTo(new Literal(id));

            instructions.Where(
                instructions.Criteria.Count > 0
                    ?
                        criteria.ToClause() & instructions.Criteria
                    :
                        criteria.ToClause()
                );

            return GetInstance(instructions);
        }

        /// <summary>
        /// Retrieve an instance based on the search criteria provided
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="keyList">Identifies the properties which uniquely identify an instance</param>
        /// <param name="ids">The values for each key to filter the record by</param>
        /// <returns>The first instance matching the criteria provided</returns>
        public override T GetInstance<T>(string[] keyList, object[] ids)
        {
            return GetInstance(keyList, ids, new LoadInstruction<T>());
        }

        /// <summary>
        /// Retrieve an instance based on the search criteria provided
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="keyList">Identifies the properties which uniquely identify an instance</param>
        /// <param name="ids">The values for each key to filter the record by</param>
        /// <param name="instructions">Specifies how records should be loaded and populated</param>
        /// <returns>The first instance matching the criteria provided</returns>
        public override T GetInstance<T>(string[] keyList, object[] ids, LoadInstruction<T> instructions)
        {
            var type = typeof (T);
            var bindingMap = _mapIndex[_repositoryName, type];
            var keyAssignments = bindingMap.GetAssignmentsByPredicate(a => keyList.Contains(a.Target.Field));
            var keyValueLookup = new Dictionary<string, object>();

            for (int i = 0; i < keyList.Length - 1; i++)
            {
                keyValueLookup.Add(keyList[i], ids[i]);
            }

            var criteria = new CriteriaClause(
                keyAssignments.Select(k =>
                                      Criterion.Column(k.Source.Field, k.Source.Entity)
                                          .EqualTo(new Literal(keyValueLookup[k.Target.Field]))
                                          .And()
                    )
                    .Cast<IClause>()
                    .ToList()
                );

            instructions.Where(
                instructions.Criteria.Count > 0
                    ?
                        criteria & instructions.Criteria
                    :
                        criteria
                );

            return GetInstance(instructions);
        }

        /// <summary>
        /// Retrieve a complete collection of instances from the data source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <returns>The complete collection of instances from the source</returns>
        public override IList<T> GetCollection<T>()
        {
            var instruction = new LoadInstruction<T>();
            return GetCollection(instruction);
        }

        /// <summary>
        /// Retrieve a complete collection of instances from the data source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="instructions">Specifies how records should be loaded and populated</param>
        /// <returns>The complete collection of instances from the source</returns>
        public override IList<T> GetCollection<T>(LoadInstruction<T> instructions)
        {
            return Fetch<T>(instructions).ToList();
        }

        /// <summary>
        /// Inserts or updates the record represented by the instance
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="instance">The instance to persist</param>
        public override int Persist<T>(T instance)
        {
            UnitOfWork.Persist(instance, true);
            return 0;
        }

        /// <summary>
        /// Saves a collection of instances by inserting or updating each one in the data source
        /// </summary>
        /// <typeparam name="T">The business object type which represents data in the data source</typeparam>
        /// <param name="collection">The collection of instances to persist</param>
        public override int Persist<T>(IEnumerable<T> collection)
        {
            int affected = 0;
            collection.ForEach(i => affected += Persist(i));
            return affected;
        }

        #endregion

        #region Post Processing

        #endregion

        //I think this code being here is smelly, it needs a better home - 7/19/09
        protected bool BuildParentClause<S, T>(S subject, LoadInstruction<T> instruction)
        {
            var isManyToMany = false;
            var subjectType = typeof (S);
            var relativeType = typeof (T);
            var subjectMap = _mapIndex[_repositoryName, subjectType];
            var relativeMap = _mapIndex[_repositoryName, relativeType];
            var oneToManyRelationship =
                subjectMap.GetRelationshipForType<OneToManyRelationship>(relativeType) ??
                relativeMap.GetRelationshipForType<OneToManyRelationship>(subjectType);

            ManyToManyRelationship manyToManyRelationship = null;

            // check for many to many relationship
            if (oneToManyRelationship == null)
            {
                manyToManyRelationship = subjectMap.GetRelationshipForType<ManyToManyRelationship>(relativeType);
                isManyToMany = true;
            }

            if (oneToManyRelationship == null &&
                manyToManyRelationship == null)
            {
                throw new RepositoryException("No relationship has been defined between {0} and {1}", subjectType.FullName, relativeType.FullName);
            }

            if (isManyToMany)
                GetManyToManyFilter(subjectMap, relativeMap, manyToManyRelationship, subject, instruction);
            else
                GetOneToManyFilter(subjectMap, relativeMap, oneToManyRelationship, subject, instruction);

            return isManyToMany;
        }

        //I think this code being here is smelly, it needs a better home - 7/19/09
        private void GetManyToManyFilter<S, T>(IBindingMap map, IBindingMap relativeMap, ManyToManyRelationship relationship, S subject, LoadInstruction<T> instruction)
        {
            var container = relationship.Container;

            var criteriaList = new List<IClause>();

            for (int i = 0; i < container.SubjectPairs.Count; i++)
            {
                var subjectPair = container.SubjectPairs[i];
                var relativePair = container.RelativePairs[i];
                var relativeRoot = map.GetRoot();
                var subjectRoot = relativeMap.GetRoot();
                var relativeAssignment = relativeMap.GetAssignmentByTargetField(subjectRoot.SourceName, subjectPair.Target);
                var subjectAssignment = map.GetAssignmentByTargetField(relativeRoot.SourceName, relativePair.Target);
                var join =
                    Table.InnerJoin(container.ContainerName)
                        .On(
                        Criterion
                            .Column(relativeAssignment.Source.Field, relativeAssignment.Source.Entity)
                            .EqualTo(Column.Named(relativePair.Source).OwnedBy(container.ContainerName))
                        );

                var where = Criterion.Column(subjectPair.Source, container.ContainerName)
                    .EqualTo(new Literal(Reflector.Read(subject, subjectAssignment.Target.Field)));

                instruction.JoinTable(subjectRoot.SourceName, join);
                criteriaList.Add(where.And());
            }

            var parentFilter = new CriteriaClause(criteriaList);

            instruction.Where(
                instruction.Criteria.Count > 0 ?
                                                   instruction.Criteria & parentFilter :
                                                                                           parentFilter
                );
        }

        //I think this code being here is smelly, it needs a better home - 7/19/09
        protected void GetOneToManyFilter<S, T>(IBindingMap subjectMap, IBindingMap relativeMap, OneToManyRelationship oneToManyRelationship, S subject, LoadInstruction<T> instruction)
        {
            var parentFilter = new CriteriaClause(
                oneToManyRelationship.Constraints.Select(c =>
                    {
                        var subjectAssignment = subjectMap.GetAssignmentsByTargetField(c.Key).FirstOrDefault();
                        var relativeAssignment = relativeMap.GetAssignmentsByTargetField(c.Value).FirstOrDefault();
                        return Criterion
                            .Column(relativeAssignment.Source.Field, relativeAssignment.Source.Entity)
                            .EqualTo(new Literal(Reflector.Read(subject, subjectAssignment.Target.Field)))
                            .And();
                    })
                    .Cast<IClause>()
                    .ToList()
                );

            instruction.Where(
                instruction.Criteria.Count > 0 ?
                                                   instruction.Criteria & parentFilter :
                                                                                           parentFilter
                );
        }

        protected void ConstructDataSet(Type targetType, DataSet set)
        {
            var map = _mapIndex[_repositoryName, targetType];
            map.Relationships.OfType<OneToManyRelationship>().ForEach(relationship =>
                {
                    var relativeTypeName =
                        relationship.RelativeType.Name;
                    var subjectTypeName = targetType.Name;
                    var parentTable =
                        set.Tables[subjectTypeName];
                    var childTable =
                        set.Tables[relativeTypeName];

                    if (childTable != null)
                    {
                        var relativeMap =
                            _mapIndex[_repositoryName, relationship.RelativeType];

                        var parentColumns = relationship
                            .Constraints
                            .Select(constraint =>
                                {
                                    var assignment = map.GetAssignmentsByTargetField(constraint.Key).FirstOrDefault();
                                    var columnName = string.Format("{0}.{1}", 
                                                            assignment.Source.Entity,
                                                            assignment.Source.Field);
                                    return parentTable.Columns[columnName];
                                }).ToArray();

                        var childColumns = relationship
                            .Constraints
                            .Select(constraint =>
                                {
                                    var assignment = relativeMap.GetAssignmentsByTargetField(constraint.Value).FirstOrDefault();
                                    var columnName = string.Format("{0}.{1}",
                                                            assignment.Source.Entity,
                                                            assignment.Source.Field);
                                    return childTable.Columns[columnName];
                                }).ToArray();

                        set.Relations.Add(relativeTypeName, parentColumns, childColumns, false);
                    }
                });

            map.Relationships.OfType<ManyToManyRelationship>().ForEach(relationship =>
                {
                    var relativeTypeName = relationship.RelativeType.Name;
                    var subjectTypeName = targetType.Name;
                    var parentTable = set.Tables[subjectTypeName];
                    var childTable = set.Tables[relativeTypeName];

                    if (childTable != null)
                    {
                        var relativeMap = _mapIndex[_repositoryName, relationship.RelativeType];

                        var parentColumns = relationship
                            .Container
                            .SubjectPairs
                            .Select(pair =>
                                {
                                    var assignment = map.GetAssignmentsByTargetField(pair.Target).FirstOrDefault();
                                    var columnName = string.Format("{0}.{1}", assignment.Source.Entity, assignment.Source.Field);
                                    return parentTable.Columns[columnName];
                                }).ToArray();

                        var childColumns = relationship
                            .Container
                            .SubjectPairs
                            .Select(pair =>
                                {
                                    var assignment = map.GetAssignmentsByTargetField(pair.Target).FirstOrDefault();
                                    var columnName = string.Format("{0}.{1}", relationship.Container.ContainerName, assignment.Source.Field);
                                    return childTable.Columns[columnName];
                                }).ToArray();

                        set.Relations.Add(relativeTypeName, parentColumns, childColumns, false);
                    }
                });
        }

        protected IEnumerable<T> Fetch_OLD<T>(ILoadInstruction instructions) where T : class, new()
        {
            var type = typeof (T);
            var visitor = new SelectVisitor(instructions, _database.Provider, RepositoryMapIndex);
            var navigator = new PersistableTypeNavigator(type, instructions, RepositoryMapIndex);
            navigator.Accept(visitor);
            var result = visitor.GetQuery();
            var tables = visitor.TableNames;
            var set = _database.GetSet(result, "set", tables, _repositoryMapIndex);

            //then we get the root table...
            var mainTable = (DataRecordCollection) set.Tables[type.Name];
            if (mainTable.Count > 0)
            {
                var persistingBuilder = new PersistableTypeBinder<T>(RepositoryMapIndex, set);
                navigator.Accept(persistingBuilder);    
                var list = persistingBuilder.Instances;
                return list;
            }
            return new List<T>().Select(t => t);
        }

        protected IEnumerable<T> Fetch<T>(ILoadInstruction instructions) where T : class, new()
        {
            var type = typeof(T);
            var visitor = new SelectVisitor(instructions, _database.Provider, RepositoryMapIndex);
            var navigator = new PersistableTypeNavigator(type, instructions, RepositoryMapIndex);
            navigator.Accept(visitor);
            var result = visitor.GetQuery();

            return _database.ProcessReader<T>(result, RepositoryMapIndex, r =>
            {
                var readerNavigator = new DataReaderNavigator(typeof(T), r, visitor.Visited);
                var readerVisitor = new DataReaderBinder<T>(RepositoryMapIndex);
                readerNavigator.Accept(readerVisitor);
                return readerVisitor.Instances;
            });
        }

        protected IPersistable GetPersistableInstance<T>(T instance)
        {
            return PersistenceDecorator.Create(instance);
        }

        protected IEnumerable<IPersistable> GetPersistableCollection<T>(IEnumerable<T> collection)
        {
            return collection.Select(i => PersistenceDecorator.Create(i)).Cast<IPersistable>();
        }

        public DatabaseRepository(string connectionName)
        {
            var connectionSettings = ConfigurationManager.ConnectionStrings[connectionName];
            if (connectionSettings == null)
                throw new DataInterfaceException(
                    "The connection string name '{0}' was not present in the configuration file. Please provide a valid connection string name.",
                    connectionName);

            _database = new DataInterface(connectionSettings);
            _repositoryName = connectionName;
            UnitOfWork = CreateImmediateUnitOfWork();
        }

        #region ITransactionManager implementation

        public void AbortTransaction()
        {
            if (InTransaction)
            {
                ((ITransactionComplete)UnitOfWork).Rollback();
                UnitOfWork = CreateImmediateUnitOfWork();
            }
        }

        public void BeginTransaction()
        {
            UnitOfWork = new TransactionalUnitOfWork(RepositoryMapIndex, _database);
            InTransaction = true;
        }

        public void CompleteTransaction(){
            if (InTransaction)
            {
                ((ITransactionComplete)UnitOfWork).Commit();
                UnitOfWork = CreateImmediateUnitOfWork();
            }
        }

        private IUnitOfWork CreateImmediateUnitOfWork()
        {
            return new ImmediateUnitOfWork(RepositoryMapIndex, _database);
        }

        #endregion
    }
}
